package com.example.train.common.config.security.filter;

import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.json.JSONUtil;
import com.example.train.common.config.security.IgnoreUrlsConfig;
import com.example.train.common.config.security.handler.JwtAuthFailureHandler;
import com.example.train.common.config.security.utils.JwtTokenUtil;
import com.example.train.common.constant.ErrorConstant;
import com.example.train.common.utils.RequestUtil;
import com.example.train.common.utils.SignUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 防重放攻击过滤器
 * 利用API签名校验、API限时内限制请求数，防止重放攻击
 *
 * 使用该过滤器要求API请求必须携带 sign，timestamp 两个参数
 *
 * sign 为将所有入参变为字符串，然后使用MD5加签
 * timestamp 为普通时间戳,该参数必须加入sign
 *
 * ref: https://www.cnblogs.com/Sinte-Beuve/p/12093307.html
 *      https://cloud.tencent.com/developer/article/1175758
 */
@Slf4j
public class PreventRequestReplayFilter extends OncePerRequestFilter {

    private AuthenticationFailureHandler failureHandler = new JwtAuthFailureHandler();

    @Value("${settings.prevent-request-replay.cache-timeout}")
    private long cacheTimeout;
    @Value("${api.headers.timestamp}")
    private String HEADER_TIMESTAMP;
    @Value("${api.headers.sign}")
    private String HEADER_SIGN;

    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Autowired
    private IgnoreUrlsConfig ignoreUrlsConfig;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    public final static String API_PARAMETER_SEPARATOR = "&";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {


        String timestampStr = request.getHeader(HEADER_TIMESTAMP);
        String signStr = request.getHeader(HEADER_SIGN);

        Map<String, String> requestParams = RequestUtil.getRequestParamsWithSort(request);
        log.info("request parameters: {}", JSONUtil.toJsonStr(requestParams));
        log.info("request require header: sign={}, timestamp={}", signStr,timestampStr);

        String contentType = request.getContentType();
        BodyReaderHttpServletRequestWrapper requestWrapper = null;
        String jsonBodyStr= null;
        if (MediaType.APPLICATION_JSON_VALUE.equals(contentType)) {
            requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
            jsonBodyStr = requestWrapper.getBodyString(requestWrapper);
            log.debug("request JSON body:{}", jsonBodyStr);
        }

        if (StrUtil.isBlank(timestampStr)) {
            unsuccessfulValid(request,response,
                    new AuthenticationServiceException(ErrorConstant.API_TIMESTAMP_MISSING));
            return;
        }

        if (StrUtil.isBlank(signStr)) {
            unsuccessfulValid(request,response,
                    new AuthenticationServiceException(ErrorConstant.API_SIGN_MISSING));
            return;
        }

        boolean verify = verifySign(request,requestParams,timestampStr,signStr,jsonBodyStr);
        //校验失败
        if (!verify) {
            unsuccessfulValid(request,response,
                    new AuthenticationServiceException(ErrorConstant.API_SIGN_VERIFY_ERROR));
            return;
        }

        //校验请求时间 先确保参数签名可行，即证明时间戳未被修改
        //基于timestamp的方案
        long gap = System.currentTimeMillis() - Long.parseLong(timestampStr);
        if (gap > cacheTimeout) {
            unsuccessfulValid(request,response,
                    new AuthenticationServiceException(ErrorConstant.API_TIMESTAMP_OVERTIME));
            return;
        }

        if (MediaType.APPLICATION_JSON_VALUE.equals(contentType)) {
            filterChain.doFilter(requestWrapper,response);
        }else{
            filterChain.doFilter(request,response);
        }
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        boolean ignore = ignoreUrlsConfig.isIgnoreForRequest(request);

        return ignore;
    }

    private void unsuccessfulValid(HttpServletRequest request,
                                     HttpServletResponse response, AuthenticationException failed)
            throws IOException, ServletException {
        failureHandler.onAuthenticationFailure(request, response, failed);
    }

    private boolean verifySign(HttpServletRequest request,Map<String, String> requestParams,
                               String timestampStr,String signStr,String jsonBodyStr){
        //Step1 校验签名
        String parameterStr = requestParams.entrySet().stream()
                .map(m -> m.getKey() + "=" + m.getValue()).collect(Collectors.joining(API_PARAMETER_SEPARATOR));

        //拼装原始请求参数字符串 pattern: url?timestamp=xxx&k1=v1&k2=v2...
        String origStr = URLUtil.getPath(request.getRequestURI())+"?timestamp="+timestampStr;
        if (StrUtil.isNotBlank(parameterStr)) {
            origStr = origStr + API_PARAMETER_SEPARATOR + parameterStr;
        }

        if (StrUtil.isNotBlank(jsonBodyStr)) {
            origStr = origStr + API_PARAMETER_SEPARATOR + "body=" + SignUtil.digester(jsonBodyStr);
        }

        origStr = URLUtil.decode(origStr);
        log.info("original sign string: {}", origStr);

        //使用用户名加密后的密文作为加签信息中的密钥
        String authHeader = request.getHeader(this.tokenHeader);
        String authToken = authHeader.substring(this.tokenHead.length());
        String signSecret = jwtTokenUtil.decode(authToken).getSubject();

        boolean verify = SignUtil.verify(signStr, origStr, signSecret);
        return verify;
    }
}
